# -*- coding: utf-8 -*-

import base64
import threading

import oss2
import os
import pandas as pd

from oss2 import Bucket
from typing import Dict
from typing import Set
from typing import List
from concurrent.futures import ThreadPoolExecutor

from com.maycur.utils import TimeUtils
from com.maycur.oss.common.basic_operation import OssOperation
from com.maycur.oss.common.aliyun_log_parser import AliyunLogParser, AliyunLog


class AliyunOssImageRotateLog(AliyunLog):
    ent_code: str
    resource_key: str
    orientation_before_rotate: int

    def __init__(self, log: AliyunLog):
        super().__init__()
        self.log_level = log.log_level
        self.log_topic = log.log_topic
        self.log_time = log.log_time
        self.message = log.message
        core_message = self.message[self.message.index(':') + 2:]
        core_message = core_message[:core_message.index(',')]
        core_message_parts = core_message.split(' ')
        self.resource_key = core_message_parts[0]
        self.orientation_before_rotate = int(core_message_parts[2])
        self.ent_code = self.resource_key.split('/')[0]


class OssImageRotationFixTool:
    """
    OSS图片资源旋转问题修复工具类
    """
    # 旋转图片时，备份文件名的后缀
    fix_rotate_backup = '__fix_rotate_backup'
    # 修复图片黑边时，备份文件名的后缀
    fix_crop_backup = '__fix_crop_backup'

    oss_operation: OssOperation
    bucket: Bucket

    log_oss_image_list: List[AliyunOssImageRotateLog]
    log_oss_image_dict: Dict[str, List[str]]

    db_filtered_sql_path: str
    # 通过数据库associated_with和created_at返回过滤后的待处理OSS图片列表, key为resource_key
    db_filtered_oss_image_dict: Dict[str, List[AliyunOssImageRotateLog]]

    processed_file: str = None
    processed_resource_key_set: Set[str]

    def __init__(self, oss_operation: OssOperation):
        self.oss_operation = oss_operation

        self.bucket = self.oss_operation.bucket
        self.log_oss_image_list = []
        self.log_oss_image_dict = {}
        self.db_filtered_sql_path = ''
        self.db_filtered_oss_image_dict = {}
        self.processed_resource_key_set = set()

    def get_processed_oss_keys_file(self):
        if self.processed_file is None:
            # 默认旗舰版
            self.processed_file = 'processed/ng_processed_oss_keys.txt'
        return os.path.join(os.getcwd(), f'imagerotation/{self.processed_file}')

    def _parse_db_filtered_oss_image_by_csv(self, csv_file: str):
        df = pd.read_csv(csv_file)
        for i in df.index.values:
            resource_key = df.iloc[i, 0]
            self.db_filtered_oss_image_dict[resource_key] = []

    def _parse_db_filtered_oss_image_by_package(self):
        if os.path.isdir(self.db_filtered_sql_path):
            parent_dir = self.db_filtered_sql_path
            for item in os.listdir(parent_dir):
                item_path = f'{parent_dir}/{item}'
                self._parse_db_filtered_oss_image_by_csv(item_path)

    def parse_oss_image_by_aliyun_log(self, aliyun_log_file: str):
        """
        从下载的阿里云日志文件解析需要处理的OSS图片
        :aliyun_log_file: 旗舰版阿里云日志前缀格式：ng-；钉版阿里云日志前缀格式：dt-
        """
        if self.db_filtered_sql_path is not None and self.db_filtered_sql_path != '':
            print(f'开始解析已通过SQL过滤的OSS图片resource_key文件夹: {self.db_filtered_sql_path} ...')
            self._parse_db_filtered_oss_image_by_package()

        print(f'开始解析日志文件: {aliyun_log_file} ...')
        aliyun_log_parser = AliyunLogParser(aliyun_log_file)

        # 加载已经处理过的图片
        with open(self.get_processed_oss_keys_file(), 'r') as processed_f:
            for item in processed_f.readlines():
                self.processed_resource_key_set.add(item.strip().replace('\n', ''))

        for log in aliyun_log_parser.logs:
            if log.message is not None:
                log_oss_image = AliyunOssImageRotateLog(log)
                resource_key = log_oss_image.resource_key
                # 已经手动处理过的图片，不需要再处理
                if resource_key not in self.processed_resource_key_set:
                    self.log_oss_image_list.append(log_oss_image)

                    ent_code = log_oss_image.ent_code
                    if ent_code not in self.log_oss_image_dict:
                        self.log_oss_image_dict[ent_code] = []
                    self.log_oss_image_dict[ent_code].append(f"'{log_oss_image.resource_key}'")

                    # 整理真正需要处理的图片
                    if resource_key in self.db_filtered_oss_image_dict:
                        self.db_filtered_oss_image_dict[resource_key].append(log_oss_image)

        to_process_count = 0
        for key in self.db_filtered_oss_image_dict:
            if len(self.db_filtered_oss_image_dict[key]) > 0:
                to_process_count += 1
        print(f'共有{to_process_count}张OSS图片需要进行旋转处理, 过滤前共有{len(aliyun_log_parser.logs)}张图片')

    def generate_attachment_sql(self, output_file_path: str, created_at_start: str, created_at_end: str) -> None:
        """
        根据阿里云日志，整理出附件查询语句
        由于attachment有联合索引(ent_code, resource_key)，而没有单独对resource_key建索引，因此需要按ent_code分组
        """
        if len(self.log_oss_image_dict) > 0:
            # 时间范围处理
            duration_command = ''
            if (created_at_start is not None and created_at_start != '') \
                    or (created_at_end is not None and created_at_end != ''):
                if created_at_start is not None and created_at_start != '':
                    duration_command += f"\nand created_at >= '{created_at_start}'"
                if created_at_end is not None and created_at_end != '':
                    duration_command += f"\nand created_at < '{created_at_end}'"

            with open(output_file_path, 'w') as output_f:
                for ent_code in self.log_oss_image_dict:
                    ent_code_resource_keys = self.log_oss_image_dict[ent_code]

                    resource_key_batch = ',\n'.join(ent_code_resource_keys)
                    ent_code_sql = f"""select distinct resource_key from attachment where ent_code = '{ent_code}' 
                        and resource_key in (
                        {resource_key_batch}
                        )
                        and associated_with = 'invoice'{duration_command};\n\n\n
                    """
                    output_f.write(ent_code_sql)
                output_f.flush()
                print(f'共生成了{len(self.log_oss_image_dict)}条SQL查询语句')

    def rotate_image(self, resource_key: str, rotate_angle: int):
        """
        按固定角度旋转图片资源，备份原图后，重新上传到OSS覆盖原图
        :resource_key: 图片key
        :rotate_angle: 向右旋转的角度，0~360度
        """
        if not self.oss_operation.is_image(resource_key):
            return
        image_info = self.oss_operation.get_image_info(resource_key)
        if image_info is None:
            return
        width = image_info.image_width
        height = image_info.image_height
        thread_name = threading.currentThread().getName()
        print(f'[{thread_name}] start to process: {resource_key}, image info, width: {width}, height: {height}')

        # 修复图片前，先做备份
        self.oss_operation.backup_resource(resource_key, self.fix_rotate_backup)

        resize_command = self.oss_operation.get_image_resize_command_within_4096(image_info)
        style = f'image/{resize_command}/rotate,{rotate_angle}'
        print(f'[{thread_name}] start to rotate image with style: {style}')
        encoded_resource_key = base64.urlsafe_b64encode(oss2.compat.to_bytes(resource_key))
        encoded_bucket_name = base64.urlsafe_b64encode(oss2.compat.to_bytes(self.bucket.bucket_name))
        process = "{0}|sys/saveas,o_{1},b_{2}".format(style,
                                                      oss2.compat.to_string(encoded_resource_key),
                                                      oss2.compat.to_string(encoded_bucket_name))
        self.bucket.process_object(resource_key, process)

    def transfer_image_black_padding(self, resource_key: str):
        """
        web-pro服务在修复图片旋转时，水平方向或垂直方向被切割了一部分，在另一个方向留下了黑边
        虽然对OSS图片做了反向旋转，但是黑边的位置不对时，会影响发票图片的切割参数，导致切割后的预览图片更加错乱
        该方法时用于尽量修复这种错误图片，操作前也会对OSS原图做备份，然后用处理后的图片覆盖到resource_key
        :resource_key: OSS图片资源key
        """
        if not self.oss_operation.is_image(resource_key):
            return
        current_thread_name = threading.currentThread().getName()
        image_info = self.oss_operation.get_image_info(resource_key)
        width = image_info.image_width
        height = image_info.image_height
        if width >= height:
            x = int((width - height) / 2)
            y = 0
            w = height
            h = height
            resize_w = w
            resize_h = width
        else:
            x = 0
            y = int((height - width) / 2)
            w = width
            h = width
            resize_w = height
            resize_h = h
        crop_command = f'crop,x_{x},y_{y},w_{w},h_{h}'
        # 用黑色填充到另一个方向
        resize_command = f'resize,m_pad,w_{resize_w},h_{resize_h},color_000000'

        if crop_command == '' or resize_command == '':
            print(f'[{current_thread_name}] - no need to transfer crop, image: {resource_key} does not reach max size')
            return

        # 修复图片前，先做备份
        self.oss_operation.backup_resource(resource_key, self.fix_crop_backup)

        style = f'image/{crop_command}/{resize_command}'
        print(f'[{current_thread_name}] - start to transfer crop image with style: {style}')
        encoded_resource_key = base64.urlsafe_b64encode(oss2.compat.to_bytes(resource_key))
        encoded_bucket_name = base64.urlsafe_b64encode(oss2.compat.to_bytes(self.bucket.bucket_name))
        process = "{0}|sys/saveas,o_{1},b_{2}".format(style,
                                                      oss2.compat.to_string(encoded_resource_key),
                                                      oss2.compat.to_string(encoded_bucket_name))
        self.bucket.process_object(resource_key, process)

    @staticmethod
    def get_anti_rotate_angle_by_orientation(orientation: int):
        """
        获取反向旋转图片的旋转角度
        """
        if orientation == 6:
            return 270
        elif orientation == 3:
            return 180
        elif orientation == 8:
            return 90
        else:
            return 0

    def multi_thread_batch_rotate_image(self, target_ent_code='',
                                        process_size=None,
                                        max_workers=12,
                                        log_details=False):
        """
        如果传入了target_ent_code，相当于每次只操作指定的企业
        使用多线程处理
        :target_ent_code: 指定企业编码
            如果非空，只操作指定企业，如修复恒洁卫浴数据
            如果不传入，操作db_filtered_oss_image_dict中的所有企业
        :process_size: 处理图片数量限制
        :max_workers: 最大处理线程数
        :log_details: 是否打印详细日志，如web-pro服务对同一图片进行了几次旋转操作
        """
        start_time = TimeUtils.current_timestamp()
        print(f'{TimeUtils.current_date_time_string()} - 开始处理旋转图片')
        if len(self.db_filtered_oss_image_dict) == 0:
            print(f'{TimeUtils.current_date_time_string()} - 无需处理')
            return
        with open(self.get_processed_oss_keys_file(), 'a') as processed_image_f:
            def rotate_task(oss_image: AliyunOssImageRotateLog):
                """
                处理任务：发送OSS图片处理请求，并记录到已处理文件中
                """
                task_angle = self.get_anti_rotate_angle_by_orientation(oss_image.orientation_before_rotate)
                self.rotate_image(oss_image.resource_key, task_angle)
                # 处理完1条之后，就追加到已处理文件，防止处理失败，排查问题后重新操作
                processed_image_f.write('\n' + oss_image.resource_key)
                processed_image_f.flush()
            real_process_count = 0
            if process_size is not None and process_size > 0:
                real_process_count = process_size

            print(f'{TimeUtils.current_date_time_string()} - 启动线程池，最大线程数为{max_workers}')
            with ThreadPoolExecutor(max_workers=max_workers) as pool:
                for key in self.db_filtered_oss_image_dict:
                    if real_process_count <= 0:
                        break
                    image_logs = self.db_filtered_oss_image_dict[key]
                    log_len = len(image_logs)
                    if log_len == 0:
                        if log_details:
                            print(f'key: {key} has no mapping')
                        continue
                    elif log_len > 1:
                        # 同一时刻并发请求多次，有可能只旋转了1次
                        base_angle = self.get_anti_rotate_angle_by_orientation(image_logs[0].orientation_before_rotate)
                        if base_angle * log_len % 360 != 0:
                            if log_details:
                                print(f'key: {key} rotated {log_len} times')
                        else:
                            if log_details:
                                print(f'key: {key} rotated {log_len} times, angle: {base_angle}, can be ignored')

                    item = image_logs[0]
                    angle = self.get_anti_rotate_angle_by_orientation(item.orientation_before_rotate)
                    if angle == 0:
                        continue
                    if target_ent_code is not None and target_ent_code != '':
                        # 指定处理某家企业，其他企业跳过处理
                        if item.ent_code != target_ent_code:
                            continue
                    try:
                        real_process_count -= 1
                        pool.submit(rotate_task, item)
                    except Exception as e:
                        # 抛出异常检查错误原因
                        raise e
        end_time = TimeUtils.current_timestamp()
        print(f'{TimeUtils.current_date_time_string()} - 旋转图片处理结束, 耗时: {int((end_time - start_time) / 1000)} s')


class FixStep:
    """
    图片处理操作类，在__main__中实例化并调用对应的方法
    """
    fix_tool = OssImageRotationFixTool(OssOperation())

    def generate_filter_sql(self):
        """
        第1步：从阿里云日志收集脏数据，生成过滤SQL
        注意事项：
            钉版和旗舰版的日志格式不同，导出的日志必须是csv格式
            只执行一次
        """
        self.fix_tool.parse_oss_image_by_aliyun_log('/Users/tianjun315/Desktop/ng-oss-image-logs.csv')
        # 生成的SQL，存储到指定文件中，这里可以指定attachment的created_at时间范围
        self.fix_tool.generate_attachment_sql('/Users/tianjun315/Desktop/dt-oss-image-filter.sql', '', '')

    def initial_fix_image_rotation(self):
        """"
        第2步：对过滤出的图片，首次执行图片反向旋转操作，并将已操作的图片记录到processed_file
        注意事项：
            旗舰版已操作图片记录文件：ng_processed_oss_keys.txt；钉版已操作图片记录文件：dt_processed_oss_keys.txt
            db_filtered_sql_path为文件夹，可以有多个子文件，每个文件中是resource_key列表，.csv格式
        """
        self.fix_tool.processed_file = 'processed/ng_processed_oss_keys.txt'
        self.fix_tool.db_filtered_sql_path = '/Users/tianjun315/Desktop/ng-oss-image-filtered-batch'
        self.fix_tool.parse_oss_image_by_aliyun_log('/Users/tianjun315/Desktop/ng-oss-image-logs.csv')

        self.fix_tool.multi_thread_batch_rotate_image(target_ent_code='EC2XOJP83A021K',
                                                      process_size=10000,
                                                      max_workers=3)

    def transfer_image_black_padding_by_file(self, resource_key_file: str):
        """
        第3步：修复图片不合理的黑边，批处理
        并非所有已处理的图片都需要执行，只用来处理有黑边的图片
        """
        with open(resource_key_file) as f:
            with ThreadPoolExecutor(max_workers=3) as pool:
                lines = f.readlines()
                if len(lines) == 0:
                    print('no images to fix')
                    return
                for resource_key in lines:
                    resource_key = resource_key.replace('\n', '')
                    pool.submit(self.fix_tool.transfer_image_black_padding, resource_key)

    def transfer_image_black_padding_by_key(self, resource_key: str):
        """
        第3步：修复图片不合理的黑边，只处理单张图片
        """
        self.fix_tool.transfer_image_black_padding(resource_key)


if __name__ == '__main__':
    fix_step = FixStep()
    # fix_step.transfer_image_black_padding_by_key('DTEC20032315WM0W24/d3756a60-d7fd-4d66-aa97-b106cb5cad34.jpg')
    fix_step.transfer_image_black_padding_by_file('/Users/tianjun315/Desktop/EC2107282X4NFZ8-MCLBX2022060700230.txt')
